# -*- coding: utf-8 -*-
# Editor básico de código Python.

## vipera, un diseñador de aplicaciones para Python
##     Copyright (C) 2011 Ángel Luis García García
##
##    This program is free software: you can redistribute it and/or modify
##    it under the terms of the GNU General Public License as published by
##    the Free Software Foundation, either version 3 of the License, or
##    (at your option) any later version.
##
##    This program is distributed in the hope that it will be useful,
##    but WITHOUT ANY WARRANTY; without even the implied warranty of
##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##    GNU General Public License for more details.
##
##    You should have received a copy of the GNU General Public License
##    along with this program.  If not, see <http://www.gnu.org/licenses/>.

import wx
import wx.stc as stc
import wx.lib.scrolledpanel as scrolled

import os
import keyword
import tokenize
import StringIO

from editor2_vista import panel_editor_python
from editor2_vista import dialogo_ir_a
from editor2_vista import dialogo_reemplazar
from editor2_vista import dialogo_indentacion
from i18n_vipera import t

faces = {'times': 'Courier New',
         'mono' : 'Courier New',
         'helv' : 'Courier New',
         'other': 'Courier New',
         'size' : 10,
         'size2': 10,
         'backcol'   : '#FFFFFF',
         'calltipbg' : '#FFFFB8',
         'calltipfg' : '#404040'
         }    

def cadenaWindows(cadena):
    '''Parseo de rutas en plataformas Microsoft'''
    # Si estamos en Windows y hay espacios en blanco en la cadena
    # hay que incluir comillas dobles alrededor de la cadena.
    if os.name == 'nt' and cadena.count(" ") > 0:
        cadena = "%s%s%s" % ('"', cadena, '"')
    # Devolvemos cadena.
    return cadena

def _rstrip(line, JUNK='\n \t'):
    """Return line stripped of trailing spaces, tabs, newlines."""
    i = len(line)
    while i > 0 and line[i-1] in JUNK:
        i -= 1
    return line[:i]

# Count number of leading blanks.
def getlspace(line):
    '''Count number of leading blanks'''
    i, n = 0, len(line)
    while i < n and line[i] == " ":
        i += 1
    return i

class EditorSTC(stc.StyledTextCtrl):
    '''Editor básico de código Python'''
    def __init__(self, parent, indent = 4):
        '''Constructor de clase'''
        stc.StyledTextCtrl.__init__(self, parent, -1)

        self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
        self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)

        self.SetLexer(stc.STC_LEX_PYTHON)
        self.SetKeyWords(0, " ".join(keyword.kwlist))
        self.SetProperty("fold", "1")
        self.SetProperty("tab.timmy.whinge.level", "1")
        self.SetMargins(0,0)

        self.SetViewWhiteSpace(False)
        self.SetEdgeMode(stc.STC_EDGE_BACKGROUND)
        self.SetEdgeColumn(78)
        self.SetMarginWidth(2, 12)

        # #######
        # BINDING
        # #######
      
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressed)

        # Global default styles for all languages
        self.StyleSetSpec(stc.STC_STYLE_DEFAULT,     "face:%(helv)s,size:%(size)d" % faces)
        self.StyleClearAll()  # Reset all to be like the default

        # Global default styles for all languages
        self.StyleSetSpec(stc.STC_STYLE_DEFAULT,     "face:%(helv)s,size:%(size)d" % faces)
        self.StyleSetSpec(stc.STC_STYLE_LINENUMBER,  "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces)
        self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces)
        self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,  "fore:#FFFFFF,back:#0000FF,bold")
        self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,    "fore:#000000,back:#FF0000,bold")

        # Python styles
        # Default
        self.StyleSetSpec(stc.STC_P_DEFAULT, "fore:#000000,face:%(helv)s,size:%(size)d" % faces)
        # Comments
        self.StyleSetSpec(stc.STC_P_COMMENTLINE, "fore:#007F00,face:%(other)s,size:%(size)d" % faces)
        # Number
        self.StyleSetSpec(stc.STC_P_NUMBER, "fore:#007F7F,size:%(size)d" % faces)
        # String
        self.StyleSetSpec(stc.STC_P_STRING, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces)
        # Single quoted string
        self.StyleSetSpec(stc.STC_P_CHARACTER, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces)
        # Keyword
        self.StyleSetSpec(stc.STC_P_WORD, "fore:#00007F,bold,size:%(size)d" % faces)
        # Triple quotes
        self.StyleSetSpec(stc.STC_P_TRIPLE, "fore:#7F0000,size:%(size)d" % faces)
        # Triple double quotes
        self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, "fore:#7F0000,size:%(size)d" % faces)
        # Class name definition
        self.StyleSetSpec(stc.STC_P_CLASSNAME, "fore:#0000FF,bold,underline,size:%(size)d" % faces)
        # Function or method name definition
        self.StyleSetSpec(stc.STC_P_DEFNAME, "fore:#007F7F,bold,size:%(size)d" % faces)
        # Operators
        self.StyleSetSpec(stc.STC_P_OPERATOR, "bold,size:%(size)d" % faces)
        # Identifiers
        self.StyleSetSpec(stc.STC_P_IDENTIFIER, "fore:#000000,face:%(helv)s,size:%(size)d" % faces)
        # Comment-blocks
        self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, "fore:#7F7F7F,size:%(size)d" % faces)
        # End of line where string is not closed
        self.StyleSetSpec(stc.STC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eol,size:%(size)d" % faces)
        self.SetCaretForeground("GREEN")

        self.SetMarginType(1, stc.STC_MARGIN_NUMBER)
        self.SetMarginWidth(1, 25)

        # indentación. parametrizar este elemento.
        self.SetIndent(indent)

        self.SetTabIndents(True)
        self.SetUseTabs(False)
        self.indentation = " "
        self.SetIndentationGuides(True)
            
    def OnKeyPressed(self, event):
        key     = event.GetKeyCode()
        control = event.ControlDown()
        #shift=event.ShiftDown()
        alt     = event.AltDown()
        if key == wx.WXK_RETURN and not control and not alt:
            #auto-indentation
            line        = self.GetCurrentLine()
            txt         = self.GetLine(line)
            pos         = self.GetCurrentPos()
            linePos     = self.PositionFromLine(line)
            self.CmdKeyExecute(wx.stc.STC_CMD_NEWLINE)
            indent      = self.GetLineIndentation(line)
            padding     = self.indentation * indent 
            newpos      = self.GetCurrentPos()
            # smart indentation
            stripped    = txt[:pos-linePos].split('#')[0].strip()
            firstWord   = stripped.split(" ")[0]
            self.InsertText(newpos, padding)
            newpos  += len(padding)
            self.SetCurrentPos(newpos)
            self.SetSelection(newpos, newpos)
        else:
            event.Skip()

class Reindenter(object):
    '''Clase para reindentar código Python'''
    def __init__(self, codigo_python, indent):
        '''Constructor de clase'''
        self.find_stmt = 1  # next token begins a fresh stmt?
        self.level = 0      # current indent level
        self.indent = indent 

        codigo = list()
        texto_aux = StringIO.StringIO(codigo_python)
        for i in texto_aux: 
            codigo.append(i)

        # Raw file lines.
        self.raw = codigo

        # File lines, rstripped & tab-expanded.  Dummy at start is so
        # that we can use tokenize's 1-based line numbering easily.
        # Note that a line is all-blank iff it's "\n".
        self.lines = [_rstrip(line).expandtabs() + "\n"
                      for line in self.raw]
        self.lines.insert(0, None)
        self.index = 1  # index into self.lines of next line

        # List of (lineno, indentlevel) pairs, one for each stmt and
        # comment line.  indentlevel is -1 for comment lines, as a
        # signal that tokenize doesn't know what to do about them;
        # indeed, they're our headache!
        self.stats = []

    def run(self):
        '''Ejecución del parser'''
        tokenize.tokenize(self.getline, self.tokeneater)
        # Remove trailing empty lines.
        lines = self.lines
        while lines and lines[-1] == "\n":
            lines.pop()
        # Sentinel.
        stats = self.stats
        stats.append((len(lines), 0))
        # Map count of leading spaces to # we want.
        have2want = {}
        # Program after transformation.
        after = self.after = []
        # Copy over initial empty lines -- there's nothing to do until
        # we see a line with *something* on it.
        i = stats[0][0]
        after.extend(lines[1:i])
        for i in range(len(stats)-1):
            thisstmt, thislevel = stats[i]
            nextstmt = stats[i+1][0]
            have = getlspace(lines[thisstmt])
            want = thislevel * self.indent
            if want < 0:
                # A comment line.
                if have:
                    # An indented comment line.  If we saw the same
                    # indentation before, reuse what it most recently
                    # mapped to.
                    want = have2want.get(have, -1)
                    if want < 0:
                        # Then it probably belongs to the next real stmt.
                        for j in xrange(i+1, len(stats)-1):
                            jline, jlevel = stats[j]
                            if jlevel >= 0:
                                if have == getlspace(lines[jline]):
                                    want = jlevel * self.indent
                                break
                    if want < 0:           # Maybe it's a hanging
                                            # comment like this one,
                        # in which case we should shift it like its base
                        # line got shifted.
                        for j in xrange(i-1, -1, -1):
                            jline, jlevel = stats[j]
                            if jlevel >= 0:
                                want = have + getlspace(after[jline-1]) - \
                                     getlspace(lines[jline])
                                break
                    if want < 0:
                        # Still no luck -- leave it alone.
                        want = have
                else:
                    want = 0
            assert want >= 0
            have2want[have] = want
            diff = want - have
            if diff == 0 or have == 0:
                after.extend(lines[thisstmt:nextstmt])
            else:
                for line in lines[thisstmt:nextstmt]:
                    if diff > 0:
                        if line == "\n":
                            after.append(line)
                        else:
                            after.append(" " * diff + line)
                    else:
                        remove = min(getlspace(line), -diff)
                        after.append(line[remove:])
        return self.raw != self.after

    def write(self):
        '''Escritura de nuevo código'''
        ret = ""
        for i in self.after: ret += i
        return ret

    # Line-getter for tokenize.
    def getline(self):
        '''Line-getter for tokenize'''
        if self.index >= len(self.lines):
            line = ""
        else:
            line = self.lines[self.index]
            self.index += 1
        return line

    # Line-eater for tokenize.
    def tokeneater(self, type, token, (sline, scol), end, line,
                   INDENT=tokenize.INDENT,
                   DEDENT=tokenize.DEDENT,
                   NEWLINE=tokenize.NEWLINE,
                   COMMENT=tokenize.COMMENT,
                   NL=tokenize.NL):
        '''Parseador de código'''

        if type == NEWLINE:
            # A program statement, or ENDMARKER, will eventually follow,
            # after some (possibly empty) run of tokens of the form
            #     (NL | COMMENT)* (INDENT | DEDENT+)?
            self.find_stmt = 1

        elif type == INDENT:
            self.find_stmt = 1
            self.level += 1

        elif type == DEDENT:
            self.find_stmt = 1
            self.level -= 1

        elif type == COMMENT:
            if self.find_stmt:
                self.stats.append((sline, -1))
                # but we're still looking for a new stmt, so leave
                # find_stmt alone

        elif type == NL:
            pass

        elif self.find_stmt:
            # This is the first "real token" following a NEWLINE, so it
            # must be the first token of the next program statement, or an
            # ENDMARKER.
            self.find_stmt = 0
            if line:   # not endmarker
                self.stats.append((sline, self.level))

class panel_codigo_ed_aux(panel_editor_python):
    '''Panel de edición de código Python'''
    def __init__(self, parent, indent = 4, idioma = None):
        '''Constructor de clase'''
        # Constructor padre.
        panel_editor_python.__init__(self, parent)
        # Idioma.
        self.l = idioma
        # Cambiamos idioma de widgets de wxFB.
        self.m_button_nuevo.SetToolTipString(t(u'Nuevo fichero', self.l))
        self.m_button_abrir.SetToolTipString(t(u'Abrir fichero', self.l))
        self.m_button_salvar.SetToolTipString(t(u'Salvar fichero', self.l))
        self.m_button_copiar.SetToolTipString(t(u'Copiar', self.l))
        self.m_button_cortar.SetToolTipString(t(u'Cortar', self.l))
        self.m_button_pegar.SetToolTipString(t(u'Pegar', self.l))
        self.m_button_undo.SetToolTipString(t(u'Deshacer', self.l))
        self.m_button_redo.SetToolTipString(t(u'Rehacer', self.l))
        self.m_button_indent.SetToolTipString(t(u'Nivel de indentación de código', self.l))
        self.m_button_reemplazar.SetToolTipString(t(u'Reemplazar texto', self.l))
        self.m_button_sel_all.SetToolTipString(t(u'Seleccionar todo', self.l))
        self.m_button_dedent_sel.SetToolTipString(t(u'Dedentar código seleccionado', self.l))
        self.m_button_indent_sel.SetToolTipString(t(u'Indentar código seleccionado', self.l))
        self.m_button_coment.SetToolTipString(t(u'Comentar código seleccionado', self.l))
        self.m_button_descoment.SetToolTipString(t(u'Descomentar código seleccionado', self.l))
        self.m_button_ir_a.SetLabel(t(u'IR A...', self.l))
        # Editor STC.
        self.editor = EditorSTC(self.m_panel_ed, indent)
        # Sizers.
        sizer_c = wx.BoxSizer(wx.VERTICAL)
        # Incluimos editor en el sizer.
        sizer_c.Add(self.editor, 1, wx.ALL|wx.EXPAND, 10)
        # Añadimos el sizer al panel.
        self.m_panel_ed.SetSizer(sizer_c)
        # Y el foco al editor.
        self.editor.SetFocus()

    def __aux_coment_texto(self, opcion):
        '''Comentar/descomentar área de texto seleccionado'''
        # Obtenemos texto seleccionado.
        texto = self.editor.GetSelectedText()
        texto_original = StringIO.StringIO(texto)
        texto_destino = ''
        for linea in texto_original:
            nblancos = 0
            for caracter in linea:
                if caracter == '\n': 
                    texto_destino += '\n'
                    continue
                if caracter == '\r': 
                    texto_destino += '\r'
                    continue
                if caracter == '\t': 
                    nblancos += self.editor.GetTabWidth() 
                    continue
                # Contamos los blancos.
                if caracter == ' ': 
                    nblancos += 1
                else:
                    # Formamos nueva línea comentada.
                    if opcion == 0:
                        nueva_linea = '%s#%s' % (nblancos * ' ', linea[nblancos:])
                        texto_destino += nueva_linea 
                        # Salimos de la línea.
                        break
                    if opcion == 1:
                        if caracter == '#':
                            # Formamos nueva línea descomentada.
                            nueva_linea = '%s%s' % (nblancos * ' ', linea[nblancos+1:])
                        else:
                            nueva_linea = '%s%s' % (nblancos * ' ', linea[nblancos:])
                        texto_destino += nueva_linea 
                        # Salimos de la línea.
                        break

        # Reemplazamos selección.
        self.editor.ReplaceSelection(texto_destino)
        
    def OnComent(self, event):
        '''Comentar área de texto seleccionado'''
        self.__aux_coment_texto(0)

    def OnDescoment(self, event):
        '''Descomentar área de texto seleccionado'''
        self.__aux_coment_texto(1)
    
    def OnSelTodo(self, event):
        '''Selección de todo el texto'''
        self.editor.SelectAll()
        
    def OnDedentSel(self, event):
        '''Dedentación de código seleccionado'''
        self.editor.BackTab()
        
    def OnIndentSel(self, event):
        '''Indentación de código seleccionado'''
        self.editor.Tab()
        
    def OnCopiar(self, event):
        '''Copiar'''
        self.editor.Copy()

    def OnPegar(self, event):
        '''Pegar'''
        self.editor.Paste()

    def OnCortar(self, event):
        '''Cortar'''
        self.editor.Cut()

    def OnUndo(self, event):
        '''Deshacer'''
        self.editor.Undo()

    def OnRedo(self, event):
        '''Rehacer'''
        self.editor.Redo()

    def OnIr(self, event):
        '''Ir a...'''
        a = dialogo_ir_a(self)
        a.Show()
        if a.ShowModal() == wx.ID_OK:
            linea = a.m_linea.GetValue()
            self.editor.ScrollToLine(linea - 1)
            self.editor.GotoLine(linea - 1)
            self.editor.SetFocus()
        a.Destroy()

    def __busqueda_y_reemplazo(self, opcion = "busqueda", texto_nuevo = None):
        '''Buscar y reemplazar texto'''
        texto = self.m_searchCtrl.GetValue().strip()
        if len(texto) == 0: return
        pos = self.editor.GetCurrentPos()
        if pos!=self.editor.GetLength():
            self.editor.SetCurrentPos(pos+1)
        self.editor.SearchAnchor()
        spos = 0
        while True:
            spos = self.editor.SearchNext(5,texto)
            self.editor.EnsureCaretVisible()
            if spos != -1:
                if opcion in ['busqueda']: break
                self.editor.ReplaceSelection(texto_nuevo)
                if opcion in ["reemplazar"]: break
            else:
                self.editor.SetCurrentPos(pos)
                break

    def cambiar_indentacion(self, indent = 4):
        '''Método para el cambio de indentación de código'''
        # Código Python actual.
        codigo_python = self.editor.GetText()
        # Cambiamos la indentación del editor.
        self.editor.SetIndent(indent)
        r = Reindenter(codigo_python, indent)
        if r.run():
            nuevo_codigo_python = r.write()
            # Incluimos el nuevo código.
            self.editor.SetText(nuevo_codigo_python)
            return True
        else:
            return False

    def OnIndentacion(self, event):
        '''Cambio de indentación'''
        a = dialogo_indentacion(self)
        if a.ShowModal() == wx.ID_OK:
            indent = a.m_spinCtrl_indent.GetValue()
            self.cambiar_indentacion(indent)
        a.Destroy()

    def OnReemplazar(self, event):
        '''Reemplazar texto'''
        a = dialogo_reemplazar(self)
        a.Show()
        if a.ShowModal() == wx.ID_OK:
            reemplazar_todo = a.m_checkBox_todo.Get3StateValue() 
            texto_nuevo = a.m_textCtrl_texto.GetValue()
            if len(texto_nuevo) == 0:
                wx.MessageBox(t(u"No se reemplazó nada", self.l), t(u"Atención", self.l), wx.OK)
            else:
                if reemplazar_todo == wx.CHK_CHECKED:
                    self.__busqueda_y_reemplazo('reemplazar_todo',texto_nuevo)
                if reemplazar_todo == wx.CHK_UNCHECKED:
                    self.__busqueda_y_reemplazo('reemplazar', texto_nuevo)
        # Limpiamos y nos vamos.
        a.Destroy()

    def OnBusqueda(self, event):
        '''Búsqueda y reemplazo de texto'''
        self.__busqueda_y_reemplazo()

class panel_codigo_ed(scrolled.ScrolledPanel):
    '''Panel principal del editor de código Python'''
    def __init__(self, parent, indent = 4, ocultar_boton_indent = True, 
                 opcion_nuevo = False, 
                 opcion_abrir = False,
                 opcion_salvar = False,
                 idioma = None):
        '''Constructor de clase'''
        scrolled.ScrolledPanel.__init__(self, parent, style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER)
        self.panel = panel_codigo_ed_aux(self, indent, idioma)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.ALL|wx.EXPAND,5)
        self.SetSizer(sizer)
        self.SetupScrolling()
        # Ocultar botón de indentación.
        if ocultar_boton_indent: self.panel.m_button_indent.Show(False)
        # Demás opciones para nuevo, abrir y salvar fichero.
        if not opcion_nuevo: self.panel.m_button_nuevo.Show(False)
        if not opcion_abrir: self.panel.m_button_abrir.Show(False)
        if not opcion_salvar: self.panel.m_button_salvar.Show(False)
